0.1+0.2 == 0.3? 浅谈计算机中的浮点数

0.1 + 0.2 == ?

0.1 +0.2 是等于0.3的吗?这个问题似乎有点儿弱智,但是估计大多数人回答的都是错误的。在我们的认知当中,即使是在计算机当中,0.1+0.2也应该是等于0.3的啊,不然那不就乱套了嘛。不过现在你可以打开你的编译器来测试一下。

System.out.println((0.1+0.2 == 0.3)); // false

确实是不等的。。。那是不是Java的原因?我们可以使用C++,python测试一下.

std::cout<<(0.1+0.2 == 0.3)<<std::endl; // 0
print( (0.1+0.2 == 0.3)) // False

我们发现无论是哪一种语言来说0.1+0.2都是不等于0.3的,这说明了这不是语言的问题了。那么0.1+0.2到底是多少呢?

std::cout<<std::setprecision(20)<<std::setiosflags(std::ios::fixed)<<0.1+0.2<<std::endl;

我们取了小数点二十位。输出的结果是0.30000000000000004441。这个结果果然不是0.3,后面为什么带了几个4呢?这是为什么呢?

计算机中浮点数的储存

众所周知计算机当中数据的存储都是二进制的。比如说2就是10,8就是100。如果有一个数带有小数的话,在计算中是如何存储的呢? 我们可以先来想想看如何把一个十进制小数表示成二进制。这个其实是和整数是一样的。二进制的小数点后的第一位是2-1=0.5,小数点后的第二位是2-2=0.25…以此类推。那么如何把0.1用二进制表示?和把一个整数转为二进制有点相似,小数转为二进制是使用乘以2的方式。

整数转为二进制:除以2取余

  • 100/2 = 50 ……0

  • 50/2 = 25 ……0

  • 25/2 = 12 ……1

  • 12 /2 = 6 ……0

  • 6/2 = 3 ……0

  • 3/2 = 1 ……1

  • 1/2 = 0 ……1

然后将这些余数倒着写出来。100(10) = 1100100(2)

小数转为二进制:(乘以二取整)

  • 0.1 * 2 = 0.2 ——0
  • 0.2 * 2 = 0.4 ——0
  • 0.4 * 2 = 0.8 ——0
  • 0.8 * 2 = 1.6 ——1
  • 0.6 * 2 = 1.2 ——1
  • 0.2 * 2 = 0.4 ——0(开始重复了)

这次是将这些小数正写出来。0.1(10) = 0.0 0011 0011 0011 ……

可见二进制表示0.1是一个无限循环的数,可以float就占4个字节,double就占用8个字节。计算机是不可能使用这种方式来储存浮点数的。其实0.1~0.9999这9999个数中只有15个数可以用二进制精确的表示。

我们可以看一下Java当中基本类型表示的数的范围是多少

Java中基本类型的表示范围

数据类型 字节数 二进制位数 范围 规律
byte 1 8 -128~127 -27~27-1
short 2 16 -32768~32767 -215~215-1
int 4 32 -2147483648~2147483647 -231~231-1
long 8 64 -9223372036854775808 ~ 9223372036854775807 -263~263-1
float 4 32 1.4E-45~3.4028235E38
double 8 64 4.9E-324~1.7976931348623157E308
char 2 16 0~65535 0~216-1
boolean 1 8 true或false true或false

float的大小是和int一样的,但是float表示的范围和int表示的范围比起来那可是一个天文数字。这就肯定说明了浮点数的储存是异于整型变量的。而且也必定说明浮点数是不精确的,不然如果float是绝对精确的,int还有存在的意义吗?这浮点数不也太能干了吗?

其实浮点数为什么叫浮点数就是因为浮点数是浮动的,是不确定的。(量子力学??)为什么浮动就要剖析一下浮点数存储的原理了。

浮点数的存储

浮点数的存储分为一下四个部分。Java中浮点数的存储符合IEEE754标准。浮点数使用符号位、指数域和有效位数域来存储。

类型 符号位 指数域 有效位数域
float 1位(第31位) 8位(第23~30位) 23位(第0~22位)
double 1位(第63位) 11位(第52~62位) 52位(第0~51位)
作用 表示浮点值的正负 存储指数位 存储小数值
  • 浮点数的符号位和整型变量是一样的。0表示正数,1表示负数。

  • 指数使用了偏移量的方式来表示。偏移量为2x-1(比实际值大2x-1),x表示的是指数域的位数。

任何一个非0且非无穷大的浮点数都可以表示为 v = s × m × 2e的形式。 s为-1或1,m为有效位数(小数),e为指数。

根据m和e的不同,我们可以将浮点数分为三类:

  1. 正规化浮点数 ——指数域不全为0且不全为1.

    99.5f在计算机中存储为:0 10000101 10001110000000000000000

    • 有效位数是0.1000111加上11.1000111转为十进制为1.5546875.

    • 指数部分减去偏移量(01111111)就是110也就是6.

    • 符号位为0,表示这是一个正浮点数。

    99.5f = 1 × 1.5546875 × 2 6 = 99.5(就是这么完美)

    16进制表示为 0x1.8ep6 (1.8e->1.10001110) p:代表指数位,因为e在16进制中已经被使用了。 6代表26

  2. 非正规化浮点数 ——指数域全为0且有效位数域不全为0

    5.877472E-39f在计算机中存储为:0 00000000 1000000000000000000000

    • 有效位数为0.1此时不再加上1,转为十进制为0.5
    • 指数位为 1-127为-126
    • 符号位为0,表示这是一个正浮点数。

    5.877472E-39f= 1 × 0.1 × 2-126 = 2-127(误差有点打好像。。。)

    16进制表示为0x1.0p-127

  3. 特殊浮点数

    浮点数 符号位 指数域 有效位数域
    0 0 全是0 全是0
    负0 1 全是0 全是0
    正无穷大 0 全是1 全是0
    负无穷大 1 全是1 全是0
    NaN 任意 全是1 不全为0

    所谓NaN就是 Not a Number

上面我们就基本了解了浮点数在计算机中是如何存储的了。

我们也可以使用FloatDouble类中的中如下两个方法来查看对应浮点数在计算机中的存储情况。

public static int floatToIntBits(float value); // Float类中

public static long doubleToLongBits(double value); // Double类中

// 不过以上的两个方法输出的都是十进制的值我们可以使用 Integer (Long)类中的转为二进制
public static String toBinaryString(int i);
public static String toBinaryString(long i);

// 不过还是有问题,就是前面如果有0的话,0是不会输出的,我们可以根据输出的长度进行补0

浮点数比较需要注意的地方

扯了那么多有点没的,其实还是要回到之前的那个问题上——浮点数之间的比较。0.1 + 0.2 == 0.3怎么才能让他返回true呢?

设置绝对误差

public static int compareFloat(float a, float b, double realError){
    if (Math.abs(a-b) <= realError){
        return true;
    }else {
        return false;
    }
}

不过这样真的合理吗?设置realError为 10-6

10-6好2 × 10-6返回的也是true。不过,平心而论他们的确不应该返回true。

而1000000000和1000000000.1应该返回true的却返回了false

设置相对误差

public static int compareFloat(float a, float b, double absError){
    if (Math.abs(a-b)/(a+b)*2 <= absError){
        return true;
    }
    return false;
}

不过这种相对误差其实也是有点儿问题的。

其实对于浮点值精度的丢失的解决方法是非常复杂的,而且是没有一个固定的方法的。我们只能考虑到我们 自己的需求,设置合理的误差的范围进行合理的比较。

设置绝对误差和相对误差

public static compareFloat(float a, float b, double realError, double absError){
    if (a == b){
        return true;
    }
    if (Math.abs(b-a) <= realError){
        return true;
    }
    if (Math.abs(b-a)/a+b*2 <= absError){
        return true;
    }
    return false;
}

设置好合理的realError和absError就可以确保0.1+0.2 == 0.3啦

其实关于浮点数的转换也有很多的问题,不过这里就不多说啦。。。

总结

浮点数是条蛇,别有事没事就玩它!尤其是这种代码就別写出来丢人了。。。蛤蛤蛤

for (double i = 0.1 ; i != 1.0; i += 0.1){
    // ...
}

之前看到没啥的,现在看到总是想笑~~


一枚小菜鸡